using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Diagnostics;
using System.Threading;
using Coriander.Xmpp;
using Coriander.Xmpp.Jabber;
using Coriander.Xmpp.Logging;
using System.Net.Security;
using System.IO;
using Coriander.Xmpp.Security;
using Coriander.Xmpp.Xmpp;

namespace Coriander.Xmpp
{
    /// <summary>
    /// Minimal, lightweight Xmpp connection.
    /// </summary>
    /// <remarks>
    /// See: http://code.google.com/apis/talk/open_communications.html
    /// See: http://svn.ag-software.de/ags/trunk/agsxmpp/XmppClientConnection.cs
    /// </remarks>
    public class Connection : IConnection, IDisposable
    {
        # region * Events *

        private event ReceivedEventHandler received;
        
        public delegate void ReceivedEventHandler(
            Object sender,
            ReceiveEventArgs e
        );

        /// <summary>
        /// 
        /// </summary>
        public event ReceivedEventHandler Received
        {
            add { received += value; }
            remove { received -= value; }
        }

        # endregion

        # region * Fields, properties and ctors *

        readonly Uri uri;
        readonly Socket socket = new Socket(
            AddressFamily.InterNetwork, 
            SocketType.Stream, 
            ProtocolType.Tcp
        ); 

        AsyncCallback sendCallback;
        AsyncCallback receiveCallback;

        Boolean disposed = false;

        TimeSpan closeTimeout = TimeSpan.FromSeconds(5);

        IConnectionPolicy connectionPolicy;

        readonly EventWaitHandle waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);

        readonly EventWaitHandle readWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset); 

        Boolean isBlocking = true;

        ILogger logger;

        Stream stream = null;

        ITlsProvider tlsProvider;

        ConnectionSettings settings = new ConnectionSettings(
            TimeSpan.FromMilliseconds(500), 
            TimeSpan.FromMilliseconds(500)
        );

        /// <summary>
        /// Gets or sets the connection policy to use.
        /// </summary>
        public IConnectionPolicy ConnectionPolicy
        {
            get
            {
                return connectionPolicy ??
                    (connectionPolicy = new JabberConnectionPolicy());
            }
            set { connectionPolicy = value; }
        }

        /// <summary>
        /// Gets or sets the  <see cref="ConnectionSettings">ConnectionSettings</see>.
        /// </summary>
        public ConnectionSettings Settings
        {
            get { return settings; }
            set { settings = value; }
        }

        /// <summary>
        /// 
        /// </summary>
        public ILogger Logger
        {
            get { return logger; }
            set { logger = value; }
        }

        /// <summary>
        /// Gets or sets whether this socket is blocking or not.
        /// Defaults to true.
        /// </summary>
        public Boolean IsBlocking
        {
            get { return isBlocking; }
            set { isBlocking = value; }
        }

        /// <summary>
        /// Gets or sets the Tlsprovider
        /// </summary>
        public ITlsProvider TlsProvider
        {
            get { return tlsProvider; }
            set { tlsProvider = value; }
        }

        /// <summary>
        /// Gets the wait handle for the current instance
        /// </summary>
        public WaitHandle WaitHandle
        {
            get { return waitHandle; }
        }

        /// <summary>
        /// 
        /// </summary>
        public Uri Uri
        {
            get { return uri; }
        }

        /// <summary>
        /// ctor
        /// </summary>
        private Connection() : this(null) { }

        /// <summary>
        /// ctor : Uri
        /// </summary>
        /// <param name="uri"></param>
        public Connection(Uri uri) : this(uri, true) { }

        /// <summary>
        /// ctor : Uri, Boolean
        /// </summary>
        /// <param name="uri"></param>
        public Connection(Uri uri, Boolean isBlocking)
        {
            this.uri        = uri;
            this.isBlocking = isBlocking; 
        }

        # endregion

        static readonly Object readSyncRoot = new Object();

        /// <summary>
        /// Opens a stream on the current socket.
        /// </summary>
        /// <returns></returns>
        void OpenStream()
        {
            if (stream != null)
            {
                stream.Close(); 
            }

            if (TlsProvider != null)
            {
                stream = TlsProvider.GetStream(
                    new NetworkStream(socket), 
                    Uri
                );
            }
            else 
            {
                stream = new NetworkStream(socket);
            }
        }

        /// <summary>
        /// Opens a connection
        /// </summary>
        public void Open() 
        {
            socket.Connect(uri.Host, uri.Port);
            sendCallback        = new AsyncCallback(OnSentCallback);
            receiveCallback     = new AsyncCallback(OnReceivedCallback);

            OpenStream(); 
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="duration"></param>
        void Sleep(TimeSpan duration) 
        {
            Thread.Sleep(
                Convert.ToInt32(
                    Settings.SendWait.TotalMilliseconds
                )
            );
        }

        /// <summary>
        /// Writes the supplied command to the output stream. 
        /// [!] This is considered a blocking call.
        /// </summary>
        /// <param name="message"></param>
        Byte[] SendCore(Byte[] message) 
        {
            if (null == message)
                throw new ArgumentNullException("message"); 

            if (false == socket.Connected)
                throw new InvalidOperationException("Socket not connected");  

            String msg = Encoding.ASCII.GetString(message);

            Log(OperationInfo.New("SendCore"), "Beginning blocking send...");

            //
            // Begin asynchronous send and wait
            //
            BeginSend(message).AsyncWaitHandle.WaitOne();

            Sleep(Settings.SendWait);

            Log(OperationInfo.New("SendCore"), "Send complete, starting receive...");

            //
            // Start receive and wait
            //
            IAsyncResult result = BeginReceive(new State(1024));

            Log(
                OperationInfo.New("SendCore"),
                "[async-id: {0}] Started receive, waiting...",
                result.AsyncWaitHandle.GetHashCode()
            );   

            waitHandle.WaitOne();

            Log(OperationInfo.New("SendCore"), "Async receive complete, proceeding.");

            Log(OperationInfo.New("SendCore"), "Sleeping: {0}", Settings.ReceiveWait);

            // [!] Ensure threads proceed in order
            Sleep(Settings.ReceiveWait);

            Log(OperationInfo.New("SendCore"), "Obtaining async result.");

            State state = (State)result.AsyncState;

            Byte[] bytesRead = state.GetBytes();

            Log(OperationInfo.New("SendCore"), "Returning ({0}) bytes.", bytesRead.Length);

            return bytesRead;
        }

        /// <summary>
        /// Begins asynchronous send operation
        /// </summary>
        /// <returns></returns>
        IAsyncResult BeginSend(Byte[] message) 
        {
            Log(OperationInfo.New("BeginSend"), "Sending ({0}) bytes.", message.Length);

            return stream.BeginWrite(message, 0, message.Length, sendCallback, null); 
        }

        /// <summary>
        /// Handle the end of asynchronous write operation.
        /// </summary>
        /// <param name="result"></param>
        void OnSentCallback(IAsyncResult result)
        {
            stream.EndWrite(result);

            Log(new OperationInfo("OnSentCallback"), "Send complete: {0}", result.IsCompleted);
        }

        /// <summary>
        /// Start an asynchronous read
        /// </summary>
        /// <returns></returns>
        IAsyncResult BeginReceive(State state) 
        {
            if (false == socket.Connected)
                throw new InvalidOperationException("Socket not connected");

            try
            {
                // [!] Synchronize starting read operation.
                //     This doesn't synchronize the whole async operation.
                //     TODO: Consider adding a read waithandle.
                lock (readSyncRoot)
                {
                    IAsyncResult result = 
                        stream.BeginRead(
                        state.Buffer,
                        0,
                        state.Buffer.Length,
                        receiveCallback,
                        state
                    );

                    Log(
                        OperationInfo.New("BeginReceive"), 
                        "[async-id: {0}] Started async receive...",
                        result.AsyncWaitHandle.GetHashCode()
                    );

                    return result;
                }
            }
            catch (Exception e)
            {
                // TODO, 2008-04-24, BJB: Getting overlapping read invocations
                Log(OperationInfo.New("BeginReceive"), "Error: " + e.Message);

                throw;
            }
        }

        /// <summary>
        /// Handle the end of asynchronous read operation.
        /// </summary>
        /// <param name="result"></param>
        void OnReceivedCallback(IAsyncResult result)
        {
            Int32 bytesRead = stream.EndRead(result);

            State state = (State)result.AsyncState;

            Log(
                OperationInfo.New("OnReceivedCallback"),
                "[async-id: {0}] Received ({1}) bytes.",
                result.AsyncWaitHandle.GetHashCode(),
                bytesRead
            );

            if (bytesRead > 0)
            {
                state.Append(state.Buffer, 0, bytesRead);
            }

            if (socket.Available == 0) // No more to read
            {
                Log(
                    OperationInfo.New("OnReceivedCallback"), 
                    "[async-id: {0}] Read complete: {1}",
                    result.AsyncWaitHandle.GetHashCode(),
                    state.ToString()
                );

                // Read is complete, signal waiting threads they can proceed.
                // Expect us to have threads waiting on our AsyncState object.
                if (IsBlocking)
                {
                    Log(OperationInfo.New("OnReceivedCallback"), "(0) bytes available, notifying waiting threads.");
                    waitHandle.Set();
                    Log(OperationInfo.New("OnReceivedCallback"), "waiting threads notified.");
                }

                //
                // TODO: We should really wait until the entire message has been
                // received
                //
                OnReceived(
                    new ReceiveEventArgs(
                        Encoding.ASCII.GetString(
                            state.Buffer,
                            0,
                            state.Buffer.Length
                        )
                    )
                );

                if (false == IsBlocking && bytesRead > 0)
                {
                    Log(
                        OperationInfo.New("OnReceivedCallback"),
                        "Beginning another async read. Available: {0}", 
                        socket.Available
                    );

                    BeginReceive(state);
                }
            }
            else
            {
                Log(
                    OperationInfo.New("OnReceivedCallback"),
                    "Beginning another async read. Available: {0}",
                    socket.Available
               ) ;

                // Read again
                BeginReceive(state);
            }
        }

        /// <summary>
        /// Notifies listeners that data has been received
        /// </summary>
        void OnReceived(ReceiveEventArgs e) 
        {
            ReceivedEventHandler handler = received;

            if (handler != null)
            {
                handler(this, e);
            }
        }


        #region * Logging *

        /// <summary>
        /// 
        /// </summary>
        /// <param name="message"></param>
        /// <param name="args"></param>
        void Log(OperationInfo operation, String message, params Object[] args)
        {
            // TODO: Thread-safety
            if (Logger != null)
            {
                String msg = args.Length != 0 ? 
                    String.Format(message, args) : 
                    message;

                String operationColumn = operation != null ? 
                    FormatColumn(" [" + operation.Name + "]", 25) : 
                    String.Empty;

                Logger.Write(
                    "[{0}][{4}]{1} [thread: {2}] {3}",
                    DateTime.UtcNow.ToString("yyyy/MM/dd hh:mm:ss"),
                    operationColumn,
                    Thread.CurrentThread.ManagedThreadId.ToString("00"),
                    msg,
                    DateTime.UtcNow.ToBinary()
                );
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <param name="width"></param>
        String FormatColumn(String value, Int32 width)
        {
            String formatString = String.Format("{{0,-{0}:g}}", width);
            
            return String.Format(formatString, value);
        }

        # endregion

        #region * IDisposable Members * 

        /// <summary>
        /// 
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(Boolean disposing)
        {
            if (false == disposed)
            {
                if (disposing)
                {
                    if (stream != null)
                    {
                        stream.Close();
                    }

                    if (socket != null)
                    {
                        socket.Close(closeTimeout.Seconds);
                    }

                    if (waitHandle != null)
                    {
                        waitHandle.Close();
                    }
                }

                disposed = true;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        void IDisposable.Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// 
        /// </summary>
        public void Close() 
        {
            Dispose(true);
        }

        #endregion

        #region * IConnection Members * 

        /// <summary>
        /// 
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        public byte[] Send(byte[] message)
        {
           // [!] This blocks
           return SendCore(message);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        public void SendAsync(Byte[] message)
        {
            // (1) Send
            BeginSend(message).AsyncWaitHandle.WaitOne();
 
            // (2) Begin receive
            BeginReceive(new State(1024)); 
        }

        /// <summary>
        /// 
        /// </summary>
        public void Wait() 
        {
            BeginReceive(new State(1024)); 
        }

        #endregion
    }
}